package com.zxjbyte.yiyi.framework.oss.storage.s3;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import com.zxjbyte.yiyi.framework.common.constant.StringPool;
import com.zxjbyte.yiyi.framework.common.core.okhttp.LoggerLevel;
import com.zxjbyte.yiyi.framework.common.core.okhttp.OkHttpCliHolder;
import com.zxjbyte.yiyi.framework.common.exception.ServiceException;
import com.zxjbyte.yiyi.framework.common.util.LocalDateTimeXUtil;
import com.zxjbyte.yiyi.framework.oss.enums.OssType;
import com.zxjbyte.yiyi.framework.oss.enums.PolicyType;
import com.zxjbyte.yiyi.framework.oss.factory.IOssConfigFactory;
import com.zxjbyte.yiyi.framework.oss.rule.IOssRule;
import com.zxjbyte.yiyi.framework.oss.storage.AbstractOssTemplate;
import com.zxjbyte.yiyi.framework.oss.storage.FileMetadata;
import com.zxjbyte.yiyi.framework.oss.storage.OssPutResult;
import io.minio.*;
import io.minio.http.Method;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;
import java.io.InputStream;

/**
 * s3协议模板类（推荐使用）
 * 基于S3协议的文件模板类，实现 MinIO、阿里云、腾讯云、七牛云、华为云，京东云等云服务
 *
 * @Author zhangxingjia
 * @Date 2023/5/9 18:00
 * @Version: 1.0
 */
@Slf4j
@RequiredArgsConstructor
public class S3OssTemplate extends AbstractOssTemplate implements S3OssOperations{

    private final IOssConfigFactory factory;
    private final IOssRule ossRule;
    private MinioClient client;
    private S3OssConfig config;

    private void init(){
        S3OssConfigWrapper wrapper = factory.getS3OssConfig();
        config = wrapper.getConfig();
        // 简单存储服务（又名S3）客户端，用于执行存储桶和对象操作。
        MinioClient.Builder builder = MinioClient.builder();
        builder.endpoint(config.getEndpoint());
        if(StrUtil.isNotBlank(config.getRegion())){
            builder.region(config.getRegion());
        }
        builder.credentials(config.getAccessKey(), config.getSecretKey());
        OkHttpCliHolder okHttpCliHolder = OkHttpCliHolder.defaultCreate();
        String filename = System.getenv("SSL_CERT_FILE");
        if (StrUtil.isNotBlank(filename)) {
            okHttpCliHolder.enableExternalCert(filename);
        }
        builder.httpClient(okHttpCliHolder.log(LoggerLevel.NONE).build());
        client = builder.build();
    }


    @Override
    @SneakyThrows
    public void createBucket(String bucketName) {
        if(!bucketExists(bucketName)){
            client.makeBucket(
                    MakeBucketArgs.builder().bucket(getBucketName(bucketName)).build()
            );
            client.setBucketPolicy(
                    SetBucketPolicyArgs.builder().bucket(getBucketName(bucketName)).config(getPolicyType(getBucketName(bucketName), PolicyType.READ_ONLY)).build()
            );
        }
    }

    @Override
    public boolean bucketExists(String bucketName) {
        boolean result = false;
        try {
            result = client.bucketExists(
                    BucketExistsArgs.builder().bucket(getBucketName(bucketName)).build()
            );
        } catch (Exception ignored) {
        }
        return result;
    }

    @Override
    @SneakyThrows
    public void removeBucket(String bucketName) {
        client.removeBucket(
                RemoveBucketArgs.builder().bucket(getBucketName(bucketName)).build()
        );
    }

    @Override
    public void copyFile(String bucketName, String fileName, String tBucketName) {
        copyFile(bucketName, fileName, tBucketName, fileName);
    }

    @Override
    @SneakyThrows
    public void copyFile(String bucketName, String fileName, String tBucketName, String tFileName) {
        // copyObject方法是用于在不同的存储桶之间复制对象的，它提供了一个方便的方式来实现对象的跨存储桶复制
        // 复制操作是按照指定的源对象和目标对象的路径进行的，而不会对源对象或原始桶进行任何修改或删除操作
        String targetObject  = StrUtil.equals(fileName, tFileName) ? tFileName : getFileName(tFileName);
        String targetBucket = getBucketName(tBucketName);
        client.copyObject(
                CopyObjectArgs.builder()
                        .source(CopySource.builder().bucket(getBucketName(bucketName)).object(fileName).build())
                        .bucket(targetBucket)
                        .object(targetObject)
                        .build()
        );
        // 移动到目标存储桶记录一条上传日志
        OssPutResult tResult = new OssPutResult();
        tResult.setOriginalName(FileUtil.getName(tFileName));
        tResult.setName(targetObject);
        tResult.setDomain(getOssHost(tBucketName));
        tResult.setLink(buildLink(tResult.getDomain(), targetObject, bucketName));
        // 记录上传日志
        FileMetadata fileMetadata = statFile(targetBucket, targetObject);
        ossRecordAddPublish(tResult, fileMetadata.getLength(), OssType.getStorageService(config.getEndpoint()));
    }

    @Override
    public FileMetadata statFile(String path) {
        return statFile(config.getBucketName(), path);
    }

    @Override
    @SneakyThrows
    public FileMetadata statFile(String bucketName, String fileName) {
        StatObjectResponse stat = client.statObject(
                StatObjectArgs.builder().bucket(getBucketName(bucketName)).object(fileName).build()
        );
        FileMetadata metadata = new FileMetadata();
        metadata.setName(StrUtil.isBlank(stat.object()) ? fileName : stat.object());
        metadata.setHash(Convert.toStr(stat.hashCode()));
        metadata.setContentType(stat.contentType());
        metadata.setPutTime(LocalDateTimeXUtil.toDate(stat.lastModified().toLocalDateTime()));
        metadata.setLength(stat.size());
        metadata.setLink(buildLink(getOssHost(bucketName), fileName, bucketName));
        return metadata;
    }


    @SneakyThrows
    private byte[] getFileContent(String bucketName, String fileName) {
        GetObjectResponse response = client.getObject(GetObjectArgs.builder()
                .bucket(getBucketName(bucketName)) // bucket 必须传递
                .object(fileName) // 存储对象key
                .build());
        return IoUtil.readBytes(response);
    }

    @Override
    public OssPutResult putFile(InputStream input, String fileName) {
        return putFile(input, config.getBucketName(), fileName);
    }

    @Override
    public OssPutResult putFile(MultipartFile file) {
        String originalFilename = file.getOriginalFilename();
        Assert.notBlank(originalFilename, () -> new ServiceException("Please select the upload file."));
        return putFile(file, originalFilename);
    }

    @Override
    @SneakyThrows
    public OssPutResult putFile(MultipartFile file, String bucketName, String fileName) {
        return putFile(file.getInputStream(), bucketName, fileName);
    }

    @Override
    @SneakyThrows
    public OssPutResult putFile(MultipartFile file, String fileName) {
        return putFile(file, config.getBucketName(), fileName);
    }

    @Override
    public OssPutResult putFile(byte[] fileBytes, String bucketName, String fileName) {
        return putFile(new ByteArrayInputStream(fileBytes), bucketName, fileName);
    }

    @Override
    public OssPutResult putFile(byte[] fileBytes, String fileName) {
        return putFile(fileBytes, config.getBucketName(), fileName);
    }

    @Override
    public OssPutResult putFile(InputStream input, String bucketName, String fileName) {
        return put(input, bucketName, fileName,  FileUtil.getMimeType(fileName));
    }

    @SneakyThrows
    private OssPutResult put(InputStream input, String bucketName, String fileName, String contentType){
        createBucket(bucketName);
        String originalName = fileName;
        fileName = getFileName(fileName);
        bucketName = getBucketName(bucketName);
        int available = input.available();
        client.putObject(
                PutObjectArgs.builder()
                        .bucket(bucketName)
                        .contentType(contentType)
                        .object(fileName)
                        .stream(input, available, -1)
                        .build()
        );
        IoUtil.close(input);
        OssPutResult result = new OssPutResult();
        result.setOriginalName(originalName);
        result.setName(fileName);
        result.setDomain(getOssHost(bucketName));
        result.setLink(buildLink(result.getDomain(), fileName, bucketName));
        // 记录上传日志
        ossRecordAddPublish(result, available, OssType.getStorageService(config.getEndpoint()));
        return result;
    }

    @Override
    public boolean deleteFile(String fileName) {
        return deleteFile(config.getBucketName(), fileName);
    }


    @Override
    @SneakyThrows
    public boolean deleteFile(String bucketName, String fileName) {
        client.removeObject(
                RemoveObjectArgs.builder().bucket(getBucketName(bucketName)).object(fileName).build()
        );
        // 存储文件被删除之后删除日志记录（逻辑删除）
        ossRecordDeletePublish(fileName);
        return true;
    }

    /**
     * 获取存储桶策略
     * 文档参考：
     *      https://docs.amazonaws.cn/AmazonS3/latest/userguide/access-policy-language-overview.html
     *
     * @param bucketName 存储桶名称
     * @param policyType 策略枚举
     * @return String
     */
    private String getPolicyType(String bucketName, PolicyType policyType) {
        boolean isMinio = OssType.getOssType(config.getEndpoint()) == null;
        StringBuilder builder = new StringBuilder();
        builder.append("{\n");
        builder.append("    \"Statement\": [\n");
        builder.append("        {\n");
        builder.append("            \"Action\": [\n");

        switch (policyType) {
            case WRITE_ONLY:
                if(isMinio){
                    builder.append("                \"s3:GetBucketLocation\",\n");
                }
                builder.append("                \"s3:ListBucketMultipartUploads\"\n");
                break;
            case READ_WRITE:
                if(isMinio){
                    builder.append("                \"s3:GetBucketLocation\",\n");
                }
                builder.append("                \"s3:ListBucket\",\n");
                builder.append("                \"s3:ListBucketMultipartUploads\"\n");
                break;
            default:
                /**
                 * s3:GetBucketLocation: 获取存储桶的地理位置信息。这个操作允许用户获取存储桶所在的 AWS 区域。
                 * s3:ListBucket: 列出存储桶中的对象。这个操作允许用户获取存储桶内对象的列表，但不包括对象的内容。
                 * s3:GetObject: 获取对象的内容。这个操作允许用户获取存储桶内指定对象的内容。
                 */
                if(isMinio){
                    builder.append("                \"s3:GetBucketLocation\",\n");
                }
                builder.append("                \"s3:ListBucket\",\n");
                builder.append("                \"s3:GetObject\"\n");
                break;
        }

        builder.append("            ],\n");
        builder.append("            \"Effect\": \"Allow\",\n");
        builder.append("            \"Principal\": \"*\",\n");
        builder.append("            \"Resource\": \"arn:aws:s3:::");
        builder.append(bucketName);
        builder.append("\"\n");
        builder.append("        },\n");
        if (PolicyType.READ_ONLY.equals(policyType)) {
            builder.append("        {\n");
            builder.append("            \"Action\": [\n");
            builder.append("                \"s3:ListBucket\"\n");
            builder.append("            ],\n");
            builder.append("            \"Effect\": \"Deny\",\n");
            builder.append("            \"Principal\": \"*\",\n");
            builder.append("            \"Resource\": \"arn:aws:s3:::");
            builder.append(bucketName);
            builder.append("\"\n");
            builder.append("        },\n");

        }
        builder.append("        {\n");
        builder.append("            \"Action\": ");

        switch (policyType) {
            case WRITE_ONLY:
                builder.append("[\n");
                builder.append("                \"s3:AbortMultipartUpload\",\n");
                builder.append("                \"s3:DeleteObject\",\n");
                builder.append("                \"s3:ListMultipartUploadParts\",\n");
                builder.append("                \"s3:PutObject\"\n");
                builder.append("            ],\n");
                break;
            case READ_WRITE:
                builder.append("[\n");
                builder.append("                \"s3:AbortMultipartUpload\",\n");
                builder.append("                \"s3:DeleteObject\",\n");
                builder.append("                \"s3:GetObject\",\n");
                builder.append("                \"s3:ListMultipartUploadParts\",\n");
                builder.append("                \"s3:PutObject\"\n");
                builder.append("            ],\n");
                break;
            default:
                builder.append("\"s3:GetObject\",\n");
                break;
        }

        builder.append("            \"Effect\": \"Allow\",\n");
        builder.append("            \"Principal\": \"*\",\n");
        builder.append("            \"Resource\": \"arn:aws:s3:::");
        builder.append(bucketName);
        builder.append("/*\"\n");
        builder.append("        }\n");
        builder.append("    ],\n");
        builder.append("    \"Version\": \"2012-10-17\"\n");
        builder.append("}\n");
        return builder.toString();
    }

    /**
     * 根据规则生成文件名称规则
     *
     * @param originalFilename 原始文件名
     * @return string
     */
    private String getFileName(String originalFilename) {
        return ossRule.fileName(originalFilename);
    }

    /**
     * 根据规则生成存储桶名称规则
     *
     * @param bucketName 存储桶名称
     * @return String
     */
    private String getBucketName(String bucketName) {
        return ossRule.bucketName(bucketName);
    }

    /**
     * 构建访问链接
     * @param domain
     * @param fileName
     * @return
     */
    private String buildLink(String domain, String fileName, String bucketName){
        OssType ossType = OssType.getOssType(config.getEndpoint());
        if(OssType.QINIU == ossType && StrUtil.isBlank(config.getCustomDomain())){
            /**
             * 针对七牛云的补充说明：
             * 七牛云目前s3不支持直接访问，需要携带凭证，cdn域名是可以直接访问（设置存储空间为公开）但需要额外收费，
             *  资料参考：https://developer.qiniu.com/kodo/8555/set-the-custom-cdn-to-accelerate-the-domain-name
             *
             *  关于，（暂不支持通过以上服务域名匿名访问七牛云存储，必须携带 AWS Signature）
             *  这里获取 MinIO 默认支持 v4 鉴权签名，并且 v4 鉴权签名可以向后兼容 v2 鉴权方式
             */
            return getV4PresignedObjectUrl(bucketName, fileName);
        }
        return domain + StringPool.SLASH + fileName;
    }

    @SneakyThrows
    private String getV4PresignedObjectUrl(String bucketName, String fileName){
        // 注意：预签名 URL 最大有效期为7天，MinIO 的预签名 URL 是不支持永久有效的
        // 预签名有效期参考资料：https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/API/sig-v4-authenticating-requests.html
        return client.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder().bucket(bucketName).object(fileName).method(Method.GET).expiry(GetPresignedObjectUrlArgs.DEFAULT_EXPIRY_TIME).build()
        );
    }

    /**
     * 获取域名
     * 关于可能出现的场景问题
     *
     * 默认情况域名优先使用配置的自定义域名customDomain字段
     * 常规下例如京东云，自定义域名需要绑定在已创建的存储空间上
     *
     *  场景一：
     *      如果设置了自定义域名，并且调用putFile方法时，设置的bucketName参数并非自定义域名绑定的存储空间，
     *      那么此方法返回的host值在最终拼接link存在错误的情况，建议设置自定义域名情况和bucketName进行匹配使用
     *
     * @param bucketName 存储桶名称
     * @return String
     */
    private String getOssHost(String bucketName) {
        OssType ossType = OssType.getOssType(config.getEndpoint());
        switch (ossType){
            case JDCLOUD:
                // 京东云兼容s3域名配置参考文档：https://docs.jdcloud.com/cn/object-storage-service/api/regions-and-endpoints
                // 格式：<Schema>://<Bucket>.<外网Endpoint>/<Object>
            case BCE_BOS:
                // 百度云兼容s3域名配置，参考文档：https://cloud.baidu.com/doc/BOS/s/xjwvyq9l4
            case KSYUN_CS:
                // 金山云兼容s3域名配置，参考文档：https://docs.ksyun.com/documents/6761?type=3
            case QINIU:
                // 七牛云兼容s3域名配置，参考文档：https://developer.qiniu.com/kodo/4088/s3-access-domainname
            case HUAWEI_OBS:
                // OBS桶访问域名的结构为：BucketName.Endpoint。其中BucketName为桶名称，Endpoint为桶所在区域的终端节点（区域域名）。
            case ALI_OSS:
                // 访问域名结构为BucketName.Endpoint。BucketName为您的存储空间名称，Endpoint为存储空间对应的地域域名。
                if(StrUtil.isNotBlank(config.getCustomDomain())){
                    return config.getCustomDomain();
                }
                // 阿里云兼容s3域名，可以达到无缝切换，其符合AWS S3的 Bucket Virtual Hosting风格
                String schema = StrUtil.startWithIgnoreCase(config.getEndpoint(), "https://") ? "https://" : "http://";
                return schema + bucketName + StringPool.DOT + StrUtil.replaceFirst(config.getEndpoint(), schema, StringPool.EMPTY);
            case TENCENT:
                if(StrUtil.isNotBlank(config.getCustomDomain())){
                    return config.getCustomDomain();
                }
                // 腾讯云兼容s3域名配置参考文档：https://cloud.tencent.com/document/product/436/41284
                String regionName = config.getRegion();
                Assert.notBlank(regionName, () -> new ServiceException("Tencent cos Region cannot be empty."));
                if(!regionName.startsWith("cos.")){
                    regionName += "cos." + regionName;
                }
                return "https://" + regionName + ".myqcloud.com";
            default:
                // 默认为自建minio 访问风格：Path Style
                String minioSchema = StrUtil.startWithIgnoreCase(config.getEndpoint(), "https://") ? "https://" : "http://";
                return minioSchema + StrUtil.replaceFirst(config.getEndpoint(), minioSchema, StringPool.EMPTY) + StringPool.SLASH + bucketName;
        }
    }

}
